﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using QuantumConcepts.Formats.StereoLithography;

namespace QuantumConcepts.Formats.StereoLithography
{
    /// <summary>The outer-most STL object which contains the <see cref="Facets"/> that make up the model.</summary>
    public class STLDocument : IEquatable<STLDocument>, IEnumerable<Facet>
    {
        /// <summary>Defines the buffer size to use when reading from a <see cref="StreamReader"/>.</summary>
        private const int DefaultBufferSize = 1024;

        /// <summary>The name of the solid.</summary>
        /// <remarks>This property is not used for binary STLs.</remarks>
        public string Name { get; set; }

        /// <summary>The list of <see cref="Facet"/>s within this solid.</summary>
        public IList<Facet> Facets { get; set; }

        /// <summary>Creates a new, empty <see cref="STLDocument"/>.</summary>
        public STLDocument()
        {
            this.Facets = new List<Facet>();
        }

        /// <summary>Creates a new <see cref="STLDocument"/> with the given <paramref name="name"/> and populated with the given <paramref name="facets"/>.</summary>
        /// <param name="name">
        ///     The name of the solid.
        ///     <remarks>This property is not used for binary STLs.</remarks>
        /// </param>
        /// <param name="facets">The facets with which to populate this solid.</param>
        public STLDocument(string name, IEnumerable<Facet> facets)
            : this()
        {
            this.Name = name;
            this.Facets = facets.ToList();
        }

        /// <summary>Writes the <see cref="STLDocument"/> as text to the provided <paramref name="stream"/>.</summary>
        /// <param name="stream">The stream to which the <see cref="STLDocument"/> will be written.</param>
        public void WriteText(Stream stream)
        {
            using (StreamWriter writer = new StreamWriter(stream, Encoding.ASCII, DefaultBufferSize, true))
            {
                //Write the header.
                writer.WriteLine(this.ToString());

                //Write each facet.
                this.Facets.ForEach(o => o.Write(writer));

                //Write the footer.
                writer.Write("end{0}".Interpolate(this.ToString()));
            }
        }

        /// <summary>Writes the <see cref="STLDocument"/> as binary to the provided <paramref name="stream"/>.</summary>
        /// <param name="stream">The stream to which the <see cref="STLDocument"/> will be written.</param>
        public void WriteBinary(Stream stream)
        {
            using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true))
            {
                byte[] header = Encoding.ASCII.GetBytes("Binary STL generated by STLdotNET. QuantumConceptsCorp.com");
                byte[] headerFull = new byte[80];

                Buffer.BlockCopy(header, 0, headerFull, 0, Math.Min(header.Length, headerFull.Length));

                //Write the header and facet count.
                writer.Write(headerFull);
                writer.Write((UInt32)this.Facets.Count);

                //Write each facet.
                this.Facets.ForEach(o => o.Write(writer));
            }
        }

        /// <summary>Writes the <see cref="STLDocument"/> as text to the provided <paramref name="path"/>.</summary>
        /// <param name="path">The absolute path where the <see cref="STLDocument"/> will be written.</param>
        public void SaveAsText(string path)
        {
            if (path.IsNullOrEmpty())
                throw new ArgumentNullException("path");

            Directory.CreateDirectory(Path.GetDirectoryName(path));

            using (Stream stream = File.Create(path))
                WriteText(stream);
        }

        /// <summary>Writes the <see cref="STLDocument"/> as binary to the provided <paramref name="path"/>.</summary>
        /// <param name="path">The absolute path where the <see cref="STLDocument"/> will be written.</param>
        public void SaveAsBinary(string path)
        {
            if (path.IsNullOrEmpty())
                throw new ArgumentNullException("path");

            Directory.CreateDirectory(Path.GetDirectoryName(path));

            using (Stream stream = File.Create(path))
                WriteBinary(stream);
        }

        /// <summary>Appends the provided facets to this instance's <see cref="Facets"/>.</summary>
        /// <remarks>An entire <see cref="STLDocument"/> can be passed to this method and all of the facets which it contains will be appended to this instance.</remarks>
        /// <param name="facets">The facets to append.</param>
        public void AppendFacets(IEnumerable<Facet> facets)
        {
            foreach (Facet facet in facets)
                this.Facets.Add(facet);
        }

        /// <summary>Determines if the <see cref="STLDocument"/> contained within the <paramref name="stream"/> is text-based.</summary>
        /// <remarks>The <paramref name="stream"/> will be reset to position 0.</remarks>
        /// <param name="stream">The stream which contains the STL data.</param>
        /// <returns>True if the <see cref="STLDocument"/> is text-based, otherwise false.</returns>
        public static bool IsText(Stream stream)
        {
            const string solid = "solid";

            byte[] buffer = new byte[5];
            string header = null;

            //Reset the stream to tbe beginning and read the first few bytes, then reset the stream to the beginning again.
            stream.Seek(0, SeekOrigin.Begin);
            stream.Read(buffer, 0, buffer.Length);
            stream.Seek(0, SeekOrigin.Begin);

            //Read the header as ASCII.
            header = Encoding.ASCII.GetString(buffer);

            return solid.Equals(header, StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>Determines if the <see cref="STLDocument"/> contained within the <paramref name="stream"/> is binary-based.</summary>
        /// <remarks>The <paramref name="stream"/> will be reset to position 0.</remarks>
        /// <param name="stream">The stream which contains the STL data.</param>
        /// <returns>True if the <see cref="STLDocument"/> is binary-based, otherwise false.</returns>
        public static bool IsBinary(Stream stream)
        {
            return !IsText(stream);
        }

        /// <summary>Reads the <see cref="STLDocument"/> contained within the <paramref name="stream"/> into a new <see cref="STLDocument"/>.</summary>
        /// <remarks>This method will determine how to read the <see cref="STLDocument"/> (whether to read it as text or binary data).</remarks>
        /// <param name="stream">The stream which contains the STL data.</param>
        /// <param name="tryBinaryIfTextFailed">Set to true to try read as binary if reading as text results in zero facets</param>
        /// <returns>An <see cref="STLDocument"/> representing the data contained in the stream or null if the stream is empty.</returns>
        public static STLDocument Read(Stream stream,bool tryBinaryIfTextFailed=false)
        {
            //Determine if the stream contains a text-based or binary-based <see cref="STLDocument"/>, and then read it.
            var isText = IsText(stream);
            STLDocument textStlDocument = null;
            if (isText)
            {
                using (StreamReader reader = new StreamReader(stream, Encoding.ASCII, true, DefaultBufferSize, true))
                {
                    textStlDocument = Read(reader);
                }
                
                if (textStlDocument.Facets.Count > 0 || !tryBinaryIfTextFailed) return textStlDocument;
                stream.Seek(0, SeekOrigin.Begin);
            }

            //Try binary if zero Facets were read and tryBinaryIfTextFailed==true
            using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII, true))
            {
                var binaryStlDocument = Read(reader);

                //return text reading result if binary reading also failed and tryBinaryIfTextFailed==true
                return (binaryStlDocument.Facets.Count>0 || !isText)?binaryStlDocument:textStlDocument;
            }
        }

        /// <summary>Reads the STL document contained within the <paramref name="stream"/> into a new <see cref="STLDocument"/>.</summary>
        /// <remarks>This method expects a text-based STL document to be contained within the <paramref name="reader"/>.</remarks>
        /// <param name="reader">The reader which contains the text-based STL data.</param>
        /// <returns>An <see cref="STLDocument"/> representing the data contained in the stream or null if the stream is empty.</returns>
        public static STLDocument Read(StreamReader reader)
        {
            const string regexSolid = @"solid\s+(?<Name>[^\r\n]+)?";

            if (reader == null)
                return null;

            //Read the header.
            string header = reader.ReadLine();
            Match headerMatch = Regex.Match(header, regexSolid);
            STLDocument stl = null;
            Facet currentFacet = null;

            //Check the header.
            if (!headerMatch.Success)
                throw new FormatException("Invalid STL header, expected \"solid [name]\" but found \"{0}\".".Interpolate(header));

            //Create the STL and extract the name (optional).
            stl = new STLDocument()
            {
                Name = headerMatch.Groups["Name"].Value
            };

            //Read each facet until the end of the stream.
            while ((currentFacet = Facet.Read(reader)) != null)
                stl.Facets.Add(currentFacet);

            return stl;
        }

        /// <summary>Reads the STL document contained within the <paramref name="stl"/> parameter into a new <see cref="STLDocument"/>.</summary>
        /// <param name="stl">A string which contains the STL data.</param>
        /// <returns>An <see cref="STLDocument"/> representing the data contained in the <paramref name="stl"/> parameter or null if the parameter is empty.</returns>
        public static STLDocument Read(string stl)
        {
            if (stl.IsNullOrEmpty())
                return null;

            using (MemoryStream stream = new MemoryStream(Encoding.ASCII.GetBytes(stl)))
                return Read(stream);
        }

        /// <summary>Reads the STL document located at the <paramref name="path"/> into a new <see cref="STLDocument"/>.</summary>
        /// <param name="path">A full path to a file which contains the STL data.</param>
        /// <returns>An <see cref="STLDocument"/> representing the data contained in the file located at the <paramref name="path"/> specified or null if the parameter is empty.</returns>
        public static STLDocument Open(string path)
        {
            if (path.IsNullOrEmpty())
                throw new ArgumentNullException("path");

            using (Stream stream = File.OpenRead(path))
                return Read(stream);
        }

        /// <summary>Reads the STL document contained within the <paramref name="stream"/> into a new <see cref="STLDocument"/>.</summary>
        /// <remarks>This method will expects a binary-based <see cref="STLDocument"/> to be contained within the <paramref name="reader"/>.</remarks>
        /// <param name="reader">The reader which contains the binary-based STL data.</param>
        /// <returns>An <see cref="STLDocument"/> representing the data contained in the stream or null if the stream is empty.</returns>
        public static STLDocument Read(BinaryReader reader)
        {
            if (reader == null)
                return null;

            byte[] buffer = new byte[80];
            STLDocument stl = new STLDocument();
            Facet currentFacet = null;

            //Read (and ignore) the header and number of triangles.
            buffer = reader.ReadBytes(80);
            reader.ReadBytes(4);

            //Read each facet until the end of the stream. Stop when the end of the stream is reached.
            while ((reader.BaseStream.Position != reader.BaseStream.Length) && (currentFacet = Facet.Read(reader)) != null)
                stl.Facets.Add(currentFacet);

            return stl;
        }

        /// <summary>Reads the <see cref="STLDocument"/> within the <paramref name="inStream"/> as text into the <paramref name="outStream"/>.</summary>
        /// <param name="inStream">The stream to read from.</param>
        /// <param name="outStream">The stream to read into.</param>
        /// <returns>The <see cref="STLDocument"/> that was copied.</returns>
        public static STLDocument CopyAsText(Stream inStream, Stream outStream)
        {
            STLDocument stl = Read(inStream);

            stl.WriteText(outStream);

            return stl;
        }

        /// <summary>Reads the <see cref="STLDocument"/> within the <paramref name="inStream"/> as binary into the <paramref name="outStream"/>.</summary>
        /// <param name="inStream">The stream to read from.</param>
        /// <param name="outStream">The stream to read into.</param>
        /// <returns>The <see cref="STLDocument"/> that was copied.</returns>
        public static STLDocument CopyAsBinary(Stream inStream, Stream outStream)
        {
            STLDocument stl = Read(inStream);

            stl.WriteBinary(outStream);

            return stl;
        }

        /// <summary>Returns the header representation of this <see cref="STLDocument"/>.</summary>
        public override string ToString()
        {
            return "solid {0}".Interpolate(this.Name);
        }

        /// <summary>Determines whether or not this instance is the same as the <paramref name="other"/> instance.</summary>
        /// <param name="other">The <see cref="STLDocument"/> to which to compare.</param>
        /// <returns>True if this instance is equal to the <paramref name="other"/> instance.</returns>
        public bool Equals(STLDocument other)
        {
            return (this.Facets.Count == other.Facets.Count
                    && this.Facets.All((i, o) => o.Equals(other.Facets[i])));
        }

        /// <summary>Iterates through the <see cref="Facets"/> collection.</summary>
        public IEnumerator<Facet> GetEnumerator()
        {
            return this.Facets.GetEnumerator();
        }

        /// <summary>Iterates through the <see cref="Facets"/> collection.</summary>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}
